<?php

/**
 * Yls 叶(Y)绿(l)素(s)，一起读Yes ;D
 *
 * @author keluo <keluo@focrs.com>
 * @copyright 2017 Focrs, Co.,Ltd
 * @since 2017-6-21
 */
class Yls {
    
    private $_remote = true; // 默认以远程方式运行框架，false表示本地调用方式 
    
    private $_configs = [
        'debug' => false, // 调试模式开关
        'rewrite' => true, // url重写开关
        'app' => '', // 应用基本目录
        'timezone' => 'PRC', // 系统运行时区
        'namespace' => [  // 命名空间映射
            'Vendor' => 'app://Vendor'
        ],
        'db' => [ // 数据库连接信息
            'default' => [ // 默认连接，如果有多个连接，请在default配置后增加
                'dsn' => 'mysql:dbname=db;host=127.0.0.1;charset=utf8',
                'username' => 'root',
                'password' => 'root',
                'prefix' => 'FS', // 表前缀
                'create' => 'fdCreate', // 记录创建时会自动为字段fdCreate赋上当前时间(默认格式：YYYY-mm-dd HH:ii:ss)
                'update' => 'fdUpdate:timestamp' // 记录更新时会自动为字段fdUpdate赋上当前Unix时间戳(如：1499137177)
            ],
        ]
    ];
    
    private $_query = [
        'version' => null,
        'param' => [],
        'condition' => [],
        'bind' => [],
        'at' => [],
        'return' => [],
        'cache' => false,
        'atomic' => false
    ];
    
    private static $_instance;
    
    private function __construct() {}
    
    public function __destruct() {}
    
    public function __clone() {}
    
    public function __call($method, $arguments) {}
    
    public static function __callstatic($method, $arguments) {
        // @todo
    }
    
    /**
     * Yls实例
     * @return Yls
     */
    public static function Instance() {
        if(!(self::$_instance instanceof self)) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }
    
    /**
     * 读取配置信息
     * @example Yls::Instance()->debug;
     * @param string $item 配置项
     * @return null|mixed
     */
    public function __get($item) {
        return isset($this->_configs[$item]) ? $this->_configs[$item] : null;
    }
    
    /**
     * 运行框架
     * @param $config 配置信息或配置文件路径
     * @param array $auth 验证头信息，本地调用时必需
     * @param array $query 查询语句，本地调用时必需
     * @throws Exception
     */
    public function Run($config, $auth = [], array $query = null) {
        if($auth && $query) $this->_remote = false;
        $this->LoadConfig($config);
        $this->BuildEnvironment();
        $this->CheckAuth($auth) || self::Response(false, 'Authentication failed');
        $query = $this->ParseQuery($query);
        for($i = 0; $i < count($query); $i++) {
            list($api, $action) = explode(':', trim($query[$i]->api, '/'));
            $version = isset($query[$i]->version) ? $query[$i]->version : $this->_query['version'];
            $this->_query['at'] = [];
            $this->_query['param'] = isset($query[$i]->param) ? $query[$i]->param : [];
            $this->_query['condition'] = isset($query[$i]->condition) ? $query[$i]->condition : [];
            $return = $this->Invoke($this->FindApi($version, $api, $action));
            $this->PushReturn($version, $api, $action, $return, isset($query[$i]->return) ? $query[$i]->return : false);
            isset($query[$i]->bind) && is_object($query[$i]->bind) && $this->PushBind($version, $api, $action, $return, $query[$i]->bind);
        }
        
        return self::Response(true, $this->_query['return'], !$this->_remote);
    }
    
    /**
     * 获取附加条件
     * 
     * @param string $item where|group|having|order|offset|size|lock
     * @return mixed
     */
    public function Condition($item = null) {
        return is_null($item) ? $this->_query['condition'] : 
            (isset($this->_query['condition']->{$item}) ? $this->_query['condition']->{$item} : false);
    }
    
    /**
     * 获取参数
     * 如果param中获取不到，则尝试从@at中获取
     * @param string $param 参数名
     * @return boolean|mixed
     */
    public function Param($param = null) {
        if($param) {
            if(!array_key_exists($param, $this->_query['param'])) return $this->At($param); // 不用isset判断主要是当$param对应的值为null时无法识别
            $value = $this->_query['param']->$param;
            if(is_string($value) && preg_match('/^::\w+/', $value) && array_key_exists($value, $this->_query['bind'])) {
                return $this->_query['bind'][$value];
            }
            return $value;
        } else {
            $params = [];
            foreach($this->_query['param'] as $param=>$value) {
                if(is_string($value) && preg_match('/^::\w+/', $value)) {
                    $properties = explode('.', $value);
                    $b = $this->_query['bind'];
                    foreach($properties as $i=>$property) {
                        $b = $this->HasProperty($property, $b) ? $this->GetProperty($property, $b) : null;
                    }
                    $params[$param] = $b;
                } else {
                    $params[$param] = $value;
                }
            }
            return $params;
        }
    }
    
    /**
     * 读取魔法变量@
     * @example Yls::Instance()->At('id')
     * @param string $name @名称
     */
    public function At($name) {
        return isset($this->_query['at'][$name]) ? $this->_query['at'][$name] : false;
    }
    
    /**
     * 返回json信息
     * @param boolean $status 结果状态
     * @param mixed $data 返回数据
     * $param boolean $return 如果当前为本地调用模式则返回数据，否则直接输出JSON格式并终止程序
     */
    public static function Response($status, $data = '', $return = false) {
        $response = json_encode([
            'status' => true === $status, // 成功状态码
            'data' => $data, // 数据
        ]);
        if($return) return $response;
        
        header('Content-type: application/json; charset=utf-8');
        die($response);
    }
    
    /**
     * 数据库操作组件
     * @param string $connect 连接标识，默认default
     * @return \Yls\Core\Db
     */
    public static function Db($connect = 'default') {
        return new \Yls\Core\Db($connect);
    }
    
    /**
     * 数据库操作组件
     * @param string 表名
     * @return \Yls\Core\Curd
     */
    public static function Curd($table) {
        return new \Yls\Core\Curd($table);
    }
    
    /**
     * 解析query
     * @param array $query 查询语句，本地调用时必需
     */
    private function ParseQuery(array $query = []) {
        // 用php://input方式保证原始数据不会发生自动转换(传统post数据不能识别false, null值)
        ($query = json_decode($this->_remote ? file_get_contents('php://input') : json_encode($query))) 
            || self::Response(false, 'Invalid query format');
        
        if(isset($query->query)) {
            $query = is_array($query->query) ? $query->query : [$query->query];
            $this->_query['atomic'] = isset($query->atomic) && $query->atomic; // 是否为原子操作
        } elseif(isset($query->api)) {
            $query = [$query];
        }
        return $query;
    }
    

    /**
     * 压入绑定参数
     * @param string $version 版本号
     * @param string $ap
     * @param string $action
     * @param stdClass $bind
     * @param mixed $return
     */
    private function PushBind($version, $api, $action, $return, stdClass $bind) {
        foreach($bind as $k=>$v) {
            $properties = trim($v, '.') ? explode('.', trim($v, '.')) : [];
            $_r = $return;
            foreach($properties as $property) {
                if($this->HasProperty($property, $_r)) {
                    $_r = $this->GetProperty($property, $_r);
                } else self::Response(false, 'Undefined index "'.$property.'" result in API: /'.$version.'/'.$api.':'.$action);
            }
            $this->_query['bind'][$k] = $_r;
        }
    }
    
    /**
     * 压入返回值
     * @param string $version 版本号
     * @param string $ap
     * @param string $action
     * @param mixed $return API返回结果
     * @param string $property 只取出结果中的某项属性
     */
    private function PushReturn($version, $api, $action, $return, $property = '') {
        if(strlen($property)) {
            $properties = trim($property, '.') ? explode('.', trim($property, '.')) : [];
            foreach($properties as $p) {
                if($this->HasProperty($p, $return)) {
                    $return = $this->GetProperty($p, $return);
                } else self::Response(false, 'No property "'.$property.'" in result API: /'.$version.'/'.$api.':'.$action);
            }
        }
    
        $this->_query['return'][] = $return;
    }
    
    private function HasProperty($property, $object) {
        if(is_object($object)) return isset($object->$property);
        if(is_array($object)) return isset($object[$property]);
        return false;
    }
    
    private function GetProperty($property, $object) {
        if(is_object($object)) return isset($object->$property) ? $object->$property : null;
        if(is_array($object)) return isset($object[$property]) ? $object[$property] : null;
        return null;
    }
    
    /**
     * @param object $context
     * @throws Exception
     */
    private function Invoke($context) {
        $Rf = new ReflectionFunction($context);
        $values = [];
        foreach($Rf->getParameters() as $Rp) {
            if($Rp->isDefaultValueAvailable()) {
                $values[] = (false !== $this->Param($Rp->name)) ? $this->Param($Rp->name) : $Rp->getDefaultValue();
            } else {
                if(false === $this->Param($Rp->name)) {
                    throw new Exception('Parameter "'.$Rp->name.'" is required');
                }
                $values[] = $this->Param($Rp->name);
            }
        }
        
        return $Rf->InvokeArgs($values);
    }
    
    /**
     * 找到对应的api文件
     * @todo 优化文件搜索算法
     * @param float $version 版本号
     * @param string $api
     * @return string|false
     */
    private function FindApi($version, $api, $action) {
        false === $version && self::Response(false, 'Api not exist: /'.$api.':'.$action);
        $path = Yls::Instance()->app.'/'.$version;
        file_exists($path) && is_dir($path) || self::Response(false, 'Api not exist: /'.$api.':'.$action);
        foreach(explode('/', $api) as $dir) {
            $next = $path.'/'.$dir;
            if((file_exists($next) && is_dir($next))) {
                $path = $next;
            } else {
                $file = null;
                array_walk(@scandir($path), function($v) use (&$file) {
                    if(0 === strpos($v, '@')) $file = $v;
                });
                if(is_null($file)) return $this->FindApi($this->GetPreviousVersion($version), $api, $action);
                $path .= '/'.$file;
                $this->_query['at'][ltrim($file, '@')] = $dir;
            }
        }
        $file = $path.'/api.php';
        if(file_exists($file) && is_file($file)) {
            $context = include($file);
            if(is_callable($context)) $context = $context();
            if(!isset($context[$action])) {
                return $this->FindApi($this->GetPreviousVersion($version), $api, $action);
            } else {
                return $context[$action];
            }
        }
        return $this->FindApi($this->GetPreviousVersion($version), $api, $action);
    }
    
    private function GetPreviousVersion($version) {
        $dirs = scandir(Yls::Instance()->app);
        foreach($dirs as $k=>$dir) {
            if(in_array($dir, ['.', '..'])) {
                unset($dirs[$k]);
            } elseif(!is_dir(Yls::Instance()->app.'/'.$dir)) {
                unset($dirs[$k]);
            }
        }
        $versions = array_values($dirs);
        foreach($versions as $k=>$v) {
            if($version == $v && $k > 0) return $versions[$k-1];
        }
        return false;
    }
    
    /**
     * 加载配置信息
     * @param string|array $configs 配置文件所在路径|配置信息
     */
    private function LoadConfig($configs) {
        !is_array($configs) && file_exists($configs) && is_file($configs) && $configs = include($configs);
        $this->_configs = array_merge($this->_configs, $configs);
        foreach($this->_configs['namespace'] as $namespace=>$path) {
            if(!is_array($path)) $path = [$path];
            array_walk($path, function(&$v) {
                $v = preg_replace(['/^app\:\/\//', '/^yls\:\/\//'], [Yls::Instance()->app.'/', dirname(__FILE__).'/'], $v);
            });
            $this->_configs['namespace'][$namespace] = $path;
        }
        $this->_configs['namespace']['Yls'] = dirname(__FILE__);
    }
    
    /**
     * 构建框架运行环境
     */
    private function BuildEnvironment() {
        version_compare(PHP_VERSION, '5.4.0', 'lt') && die('Yls requires PHP version 5.4 or higher. (The running is '.PHP_VERSION.')');
        
        date_default_timezone_set($this->timezone); // 设置系统运行环境时区
        
        // 加载自动加载类
        require dirname(__FILE__).'/Core/Autoloader.php';
        // 注册类自动加载机制
        spl_autoload_register(['\\Yls\\Core\\Autoloader', 'LoadClass']);
        
        //系统错误异常被透明捕获进行友好处理
        ini_set('display_errors', 'Off');
        set_error_handler('\\Yls\\Core\\Error::ErrorHandler', E_ALL);
        set_exception_handler('\\Yls\\Core\\Error::ExceptionHandler');
        register_shutdown_function('\\Yls\\Core\\Error::FatalHandler');
    }
    
    /**
     * 检查权限
     * @param array $auth 验证信息
     * @return boolean
     */
    private function CheckAuth(array $auth = []) {
        if($this->_remote) {
            array_walk($_SERVER, function($value, $key) use(&$auth) {
                if(preg_match('/^HTTP_YLS_(\w+)$/', $key, $match)) {
                    $auth[$match[1]] = $value;
                }
            });
        }
        if(!(isset($auth['ID']) && isset($auth['KEY']) && isset($auth['VERSION']))) return false;
        $this->_query['version'] = $auth['VERSION'];
        $credit = Yls::Instance()->client;
        if(!isset($credit[$auth['ID']])) return false;
        
        $split = explode('.', $auth['KEY']);
        if(count($split) != 2) return false;
        
        $key = sha1($auth['ID'].'YLS'.$credit[$auth['ID']].'YLS'.$split[1]).'.'.$split[1];
        return 0 === strcasecmp($key, $auth['KEY']);
    }
}